# coding:utf-8
import paramiko
import socket
from os import path
import chardet
import re
import time
from network.CustomizedException import *
from network import utils
from ping3 import ping
import logging


# 编码列表
ENCODINGS = [
    'ascii'
    'utf-8'
]


class BaseConnection(object):
    """
    定义厂商无关的SSH连接基类
    """
    def __init__(
        self,
        host=None,    # 目标设备域名
        user=None,    # 用户名
        passwd=None,  # 密码
        port=22,  # 端口
        enable="",  # enable密码
        ip=None,  # 目标设备IP
        use_keys=False,     # 是否使用私钥登录
        key_file=None,  # 私钥文件
        pkey=None,  # 私钥

        ssh_strict=False,   # 是否执行严格的已知主机确认
        system_host_keys=False,     # 是否加载默认的host keys
        alt_host_keys=False,    # 是否加载用户指定的host key
        alt_key_file="",    # 用户指定的host key文件

        tcp_timeout=60.0,   # tcp建立超时时间
        auth_timeout=60.0,  # 登录认证超时时间
        banner_timeout=5,  # 登录横幅超时时间
        keepalive=0,   # 会话保活时间间隔
        blocking_timeout=5,  # 阻止读/写操作的超时时间
        buffer_size=65535,   # 读缓冲区大小

        send_return="\n",     # 向设备发送信息的回车符
        response_return="\n",   # 返回信息的回车符
        prompt_patterns=None,   # 命令提示符集合,re.Pattern对象
        error_patterns=None,    # 命令出错提示符集合,re.Pattern对象
        encoding="ASCII",  # SSH命令回显的编码
        pagination_enable=True,  # 是否处于分页模式
        no_pagination_cmd=None,  # 取消分页显示的命令
        pagination_pattern=None,  # 分页符标识,re.Pattern对象
        next_page_cmd=None,  # 分页模式下获取下一页的命令
        log_level=None,   # 日志等级
    ):
        """
        初始化建立连接的参数

        :param str ip: 目标设备IP，如果为空则使用host

        :param str host: 目标设备域名，当ip为空时使用

        :param int port: SSH协议端口号，默认22

        :param str user: SSH用户名

        :param str passwd: SSH密码

        :param str enable: 由普通用户模式进入特权用户模式时的enable密码

        :param bool use_keys: 是否使用ssh key连接目标设备

        :param str key_file: ssh key文件

        :param paramiko.PKey pkey: ssh key对象

        :param bool ssh_strict: 是否执行严格的已知主机确认，该项为True时，自动拒绝未知主机的SSH密钥（默认False，这意味着将接受未知主机的SSH密钥）

        :param bool system_host_keys: 是否加载系统ssh主机公钥，即'~/.ssh/known_hosts'

        :param bool alt_host_keys: 是否加载用户在参数'alt_key_file'中指定的ssh主机公钥

        :param str alt_key_file: ssh主机公钥文件

        :param float tcp_timeout: ssh连接时，建立TCP连接的超时时间，默认60.0

        :param float auth_timeout: ssh连接时，认证超时时间（秒）， 默认60.0

        :param float banner_timeout: ssh连接时，横幅超时时间（秒），默认5.0

        :param float keepalive: ssh登陆后的会话保活时间间隔（秒），默认为0.0（不会尝试保持连接有效）。

        :param float blocking_timeout: 阻止读/写操作的超时时间（秒）, 默认5.0

        :param int buffer_size: 读缓冲区大小（字节），默认65535

        :param str send_return: 向设备发送信息时的回车符, 默认"\n"

        :param str response_return: 设备返回信息的回车符，默认"\n"

        :param set prompt_patterns: 用于确认通道读取结束的正则表达式模式集合，即re.Pattern对象集合。
            一般会为不同厂商/操作系统设备设置不同的默认值，如果该参数不为空，则会将其合并到默认值中，而非取代默认值。

        :param set error_patterns: 用于检查下发的命令是否出错的正则表达式模式集合，即re.Pattern对象集合。
            一般会为不同厂商/操作系统设备设置不同的默认值，如果该参数不为空，则会将其合并到默认值中，而非取代默认值。

        :param str encoding: 当接收的二进制信息无法通过chardet.detect获取编码时，会尝试使用该编码进行解码，默认utf-8

        :param bool pagination_enable: 设备是否处于分页模式, 默认True。

        :param str no_pagination_cmd: 取消分页显示的命令。若未指定该命令，或该命令执行失败，则pagination_enable为默认值。

        :param re.pattern pagination_pattern: 分页符标识，默认值None。
            一般会为不同厂商/操作系统设备设置不同的分页标识符。
            pagination_enable=True时，遇到分页标识符则剔除命令回显中的分页标识符所在行，并执行获取下一页的动作。
            若pagination_enable=True,且未指定该参数，则命令回显可能缺失。

        :param str next_page_cmd: 获取下一页命令回显的动作。
            pagination_enable=True时，遇到分页提示符则执行该动作。
            若pagination_enable=True,且未指定该参数，则命令回显可能缺失。

        :param int log_level: 日志等级, 默认为None, 即不记录日志
        """
        if ip:
            self.host = ip.strip()
        elif host:
            self.host = host.strip()

        # 初始化日志相关参数
        if log_level:
            self.logger = utils.get_logger(self.host, log_level)     # 获取logger
            paramiko.util.log_to_file = False  # 关闭paramiko默认的日志文件记录
            paramiko.util.log_to_stderr = False  # 关闭paramiko默认的标准错误输出
            paramiko_logger = logging.getLogger('paramiko')
            paramiko_logger.setLevel(log_level)  # 设置paramiko日志级别与你的logger一致
            paramiko_logger.addHandler(self.logger.handlers[0])  # 添加自定义logger的handler到paramiko logger
        else:
            self.logger = None

        # 关键参数校验
        if not ip and not host:
            msg = "Either ip or host must be set"
            if self.logger:
                self.logger.error(msg)
            raise ValueError(msg)
        self.port = int(port)
        self.user = user.strip()
        if not self.user:
            msg = "user must be set"
            if self.logger:
                self.logger.error(msg)
            raise ValueError(msg)
        self.passwd = passwd.strip()
        self.use_keys = use_keys
        if not self.passwd and self.use_keys:
            msg = "passwd is null and use_key is False"
            if self.logger:
                self.logger.error(msg)
            raise ValueError(msg)
        self.key_file = key_file
        self.pkey = pkey

        if not ssh_strict:
            self.key_policy = paramiko.AutoAddPolicy()
        else:
            self.key_policy = paramiko.RejectPolicy()
        self.enable = enable.strip()
        self.system_host_keys = system_host_keys
        self.alt_host_keys = alt_host_keys
        self.alt_key_file = alt_key_file

        self.tcp_timeout = float(tcp_timeout)
        self.auth_timeout = float(auth_timeout)
        self.banner_timeout = float(banner_timeout)
        self.keepalive = keepalive
        self.blocking_timeout = float(blocking_timeout)
        self.buffer_size = buffer_size

        self.send_return = send_return
        self.response_return = response_return
        self.prompt_patterns = prompt_patterns
        self.error_patterns = error_patterns
        self.encoding = encoding
        self.pagination_enable = pagination_enable
        self.no_pagination_cmd = no_pagination_cmd
        self.pagination_pattern = pagination_pattern
        self.next_page_cmd = next_page_cmd
        # 初始化channel锁
        # 每次执行channel write动作后锁定channel, 执行channel read动作后解锁channel，用于保证命令与回显准确对应
        self.channel_locked = True
        # 初始化完成
        if self.logger:
            self.logger.debug("initialization complete",)
    """实现上下文管理"""

    def __enter__(self):
        """通过上下文管理器建立会话"""
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        """通过上下文管理器关闭会话"""
        self.logout()

    """创建和销毁ssh会话"""

    def login(self, retry_time=0, retry_sleep=60, width=200, height=30):
        """
        创建ssh connection并做会话准备
        :param int retry_time: 登录超时后重试次数(默认不重试)
        :param int retry_sleep: 登录超时重试时的时间间隔（单位秒，默认60）
        :param int width: VT100终端窗口的宽度(characters)
        :param int height: VT100终端窗口的指定高度(characters)
        """
        self._establish_connection(retry_time, retry_sleep, width, height)
        if self.logger:
            self.logger.info(f"login to the host successfully")
        return self._session_preparation()

    def logout(self):
        """关闭并清理ssh会话"""
        try:
            self.sshclient.close()
            if self.logger:
                self.logger.info(f"logout of the host successfully")
        except Exception as e:
            if self.logger:
                self.logger.warning(f"logout of the host fail, {repr(e)}")
            pass
        finally:
            self.sshclient = None
            self.channel = None

    def _establish_connection(self, retry_time, retry_sleep, width, height):
        """创建SSH连接
        :param int retry_time: 登录超时后重试次数
        :param int retry_sleep: 登录超时重试时的时间间隔（单位秒）
        :param int width: VT100终端窗口的宽度(characters)
        :param int height: VT100终端窗口的指定高度(characters)
        """

        # 组织创建ssh连接所需参数
        ssh_connect_params = {
            "hostname": self.host,
            "port": self.port,
            "username": self.user,
            "password": self.passwd,
            "look_for_keys": self.use_keys,
            "key_filename": self.key_file,
            "pkey": self.pkey,
            "timeout": self.tcp_timeout,
            "auth_timeout": self.auth_timeout,
            "banner_timeout": self.banner_timeout,
        }
        if self.logger:
            self.logger.debug("prepare connection parameters finish")
        # 创建ssh客户端
        self.sshclient = self._build_ssh_client()
        # 创建ssh连接
        use_time = 1
        while True:
            try:
                # 创建SSH连接
                self.sshclient.connect(**ssh_connect_params)
                if self.logger:
                    self.logger.debug(f"establish the ssh connection success on {use_time}th time")
            except (TimeoutError, socket.error) as e:
                if self.logger:
                    self.logger.warning(f"establish the ssh connection fail on {use_time}th time")
                if retry_time > 0:
                    retry_time -= 1
                    use_time += 1
                    time.sleep(retry_sleep)
                    continue
                else:
                    msg = f"tried all {use_time} times, unable to establish the ssh connection:" + str(e)
                    if self.logger:
                        self.logger.error(msg)
                    self.logout()
                    if e is TimeoutError:
                        raise TimeoutException(msg)
                    else:
                        raise SocketException(msg)
            except paramiko.AuthenticationException as e:
                msg = f"unable to establish the ssh connection:" + str(e)
                if self.logger:
                    self.logger.error(msg)
                self.logout()
                raise AuthenticationException(msg)
            except Exception as e:
                msg = f"unable to connect to the host:" + str(e)
                if self.logger:
                    self.logger.error(msg)
                self.logout()
                raise LoginFailException(msg)
            else:
                break
        # 获取创建的ssh连接
        self.transport = self.sshclient.get_transport()
        if self.logger:
            self.logger.debug("get transport")
        # 重定向transport的日志
        if self.logger:
            self.transport.logger = self.logger
            self.logger.debug("set logger of transport")
        # SSH保活，以特定的时间间隔（以秒为单位）发送SSH保活数据包
        if self.keepalive:
            self.transport.set_keepalive(self.keepalive)
            if self.logger:
                self.logger.debug("set keepalive interval of transport")
        # 创建交互式shell,可创建多个，互不影响
        self.channel = self.sshclient.invoke_shell(term="vt100", width=width, height=height)
        if self.logger:
            self.logger.debug("get channel")
        # 设置shell超时时间
        self.channel.settimeout(self.blocking_timeout)
        if self.logger:
            self.logger.debug("set timeout time of channel")

    def _build_ssh_client(self):
        """创建ssh client"""
        # 创建SSHClient实例
        sshclient = paramiko.SSHClient()
        if self.logger:
            self.logger.debug("create ssh client")
        # 加载默认的host key(~/.ssh/known_hosts)
        if self.system_host_keys:
            sshclient.load_system_host_keys()
            if self.logger:
                self.logger.debug(f"load default host keys from ~/.ssh/known_hosts")
        # 若用户指定了alt_host_keys和alt_key_file则加载用户指定的host key
        if self.alt_host_keys and path.isfile(self.alt_key_file):
            sshclient.load_host_keys(self.alt_key_file)
            if self.logger:
                self.logger.debug(f"load host keys from user file {self.alt_key_file}")
        # 设置不在host key中的主机的策略（默认情况是自动添加不受信任的主机）
        sshclient.set_missing_host_key_policy(self.key_policy)
        if self.logger:
            self.logger.debug(f"set missing host policy of ssh client as {self.key_policy}")
        return sshclient

    def _session_preparation(self):
        """
        在建立连接后准备会话, 此方法处理会话早期各种设备之间出现的一些差异。
        通常包含：
        self.set_prompt()  初始化命令提示符
        self.disable_paging()   取消分页显示
        self.clear_buffer()     清空read buffer
        """
        # 设置分页相关的默认参数
        self._set_pagination_param()
        # 设置命令提示符
        self._set_prompt_patterns()
        # 设置命令错误提示符
        self._set_error_patterns()
        # 清空read buffer
        res_clear_buffer = self._clear_buffer()
        # 处理密码更改问询
        res_handle_passwd_update = self._handle_passwd_update(res=res_clear_buffer)
        # 取消分页显示
        res_disable_paging = self._disable_paging()
        #  返回登录后执行的所有操作
        res = res_clear_buffer + res_handle_passwd_update + res_disable_paging
        if self.logger:
            flag = "#"*60 + "echo split line" + "#"*60
            self.logger.debug(f"session prepare finish with device echo as:\n{flag}\n{res}\n{flag}")
        return res

    def _set_pagination_param(self):
        """
        设置设备分页相关的默认参数，若用户指定了相关参数，则优先使用用户设置的参数
        分页标识符仅在每次从缓冲区读取的命令回显的最后一行中查找
        """
        if self.logger:
            self.logger.debug(f"no_pagination_cmd:`{self.no_pagination_cmd}`")
        if self.logger:
            self.logger.debug(f"pagination_pattern:`{self.pagination_pattern}`")
        if self.logger:
            self.logger.debug(f"next_page_cmd:`{self.next_page_cmd}`")

    def _set_prompt_patterns(self):
        """
        设置已知的命令提示符正则模式元组
        命令提示符仅在每次从缓冲区读取的命令回显的最后一行中查找
        """
        patterns = set()
        if self.pagination_pattern:
            patterns.add(self.pagination_pattern)
        if self.prompt_patterns:
            patterns.update(self.prompt_patterns)
        self.prompt_patterns = patterns
        if self.logger:
            self.logger.debug(f"prompt_patterns:{self.prompt_patterns}")

    def _set_error_patterns(self):
        """
        设置已知的命令错误提示正则模式元组
        错误提示符会在命令回显的每行中查找，所以需要修饰符re.M
        """
        patterns = set()
        if self.error_patterns:
            patterns.update(self.error_patterns)
        self.error_patterns = patterns
        if self.logger:
            self.logger.debug(f"error_patterns:{self.error_patterns}")

    def _clear_buffer(self, loop_delay=0.5):
        """
        清除read buffer：读取read buffer直至timeout
        :param float loop_delay: 延迟时间
        """
        # 清空前准备
        self.channel.settimeout(self.blocking_timeout * 2)  # 设置session超时时间为正常时间的2倍，最大程度确保完全清空buffer
        if self.logger:
            self.logger.debug(f"set timeout time of channel as twice blocking_time: {self.blocking_timeout * 2}")
        if self.logger:
            self.logger.info(f"lock channel")
        # 清空buffer
        res = ""
        while self.transport.active and self.channel.active and not self.channel.closed:
            time.sleep(loop_delay)
            try:
                buff = self.channel.recv(self.buffer_size)
                if self.logger:
                    self.logger.debug(f"read {len(buff)} bytes from channel")
            except socket.timeout:
                if self.logger:
                    self.logger.debug("read channel timeout, read channel finish")
                break
            else:
                output = self._decode_res_byte(buff)
                res += output
        else:
            msg = f"connection to the host closed"
            if self.logger:
                self.logger.error(msg)
            raise SocketException(msg)
        # 结束前准备
        self.channel.settimeout(self.blocking_timeout)  # 恢复session超时时间
        if self.logger:
            self.logger.debug(f"restore timeout time of channel as blocking_time: {self.blocking_timeout}")
        self.channel_locked = False     # 解锁channel
        if self.logger:
            self.logger.info(f"release channel")
        # 返回结果
        return res

    def _handle_passwd_update(self, res):
        """
        处理密码更改问询
        base_connection无法判断是否存在密码更改问询
        """
        msg = "BaseConnection won't handle with password update"
        if self.logger:
            self.logger.warning(msg)
        return ""

    def _disable_paging(self):
        """
        取消分页显示
        分页模式下,若您设置了pagination_pattern或next_page_cmd，则不影响命令的执行
        """
        res = ""
        if not self.no_pagination_cmd:
            if self.logger:
                self.logger.debug("no_pagination_cmd is None, won't disable paging")
            return res
        else:
            # 切换至enable视图
            try:
                self.change_mode(mode="enable")
            except Exception:
                pass
            # 取消分页
            try:
                self._write_channel(cmd=self.no_pagination_cmd)
                res = self._read_channel()
                self._find_error(res)
            except Exception:
                if self.logger:
                    self.logger.warning("disable paging fail")
                return res
            else:
                self.pagination_enable = False
                if self.logger:
                    self.logger.info("disable paging success")
                return res

    """视图切换"""

    def change_mode(self, mode):
        """
        更改命令模式
        :param str mode:命令模式，可选参数：user|enable|config
        user    用户视图（可执行部分查询命令）
        enable  特权视图（可执行查询命令）
        config  配置视图(可执行配置命令)
        """
        msg = "BaseConnection unsupported function change_mode"
        if self.logger:
            self.logger.warning(msg)
        raise UnSupportedException(msg)

    """读写channel"""

    def shell_until_timeout(self, cmd, loop_delay=0.5, cmd_error=True):
        """
        执行命令并进行结果检查，命令错误则抛出异常，否则返回结果
        该方法不检查命令提示符，读缓冲区超时则认为命令回显读取结束，所以无法保证命令回显的完整性
        :param str cmd:要执行的命令
        :param float loop_delay: 延迟时间
        :param bool cmd_error: 是否对命令结果进行异常检查
        """
        # 读写channel
        if self.logger:
            self.logger.info(f"try to execute command:`{cmd}`, with param:loop_delay={loop_delay}, cmd_error={cmd_error}")
        self._write_channel(cmd)
        res = self._read_channel_until_timeout(loop_delay=loop_delay)
        # 检查是否存在命令错误
        if cmd_error:
            self._find_error(res)
        # 返回命令回显
        if self.logger:
            self.logger.info(f"return result of command '`{cmd}`'")
        return res

    def shell(self, cmd, loop_delay=0.5, strict=False, cmd_error=True):
        """
        执行命令并进行结果检查，命令错误则抛出异常，否则返回结果
        该方法根据命令提示符等尽可能保证命令回显的完整性，在读取缓冲区信息时：
            遇到命令提示符：认为命令回显读取结束
            遇到分页符：认为该页读取结束，发送获取下一页的命令后继续读取，直至遇到命令提示符或超时
            读取超时：当strict为True时抛出异常，当strict为False时，清空缓冲区，将已读到的命令回显和清空缓冲区结果一并返回
        :param str cmd:要执行的命令
        :param float loop_delay: 延迟时间
        :param bool strict: 是否严格执行命令提示符匹配
            True    无法匹配命令提示符时引发超时异常
            False   无法匹配命令提示符时仍会返回结果
        :param bool cmd_error: 是否对命令结果进行异常检查
        """
        if self.logger:
            self.logger.info(f"try to execute command:`{cmd}`, with param:loop_delay={loop_delay}, strict={strict}, cmd_error={cmd_error}")
        # 读写channel
        self._write_channel(cmd)
        if self.pagination_enable:
            if not self.pagination_pattern or not self.next_page_cmd:
                msg = "host is in pagination mode,but pagination_pattern or next_page_cmd is None"
                if self.logger:
                    self.logger.error(msg)
                raise PreconditionException(msg)
            res = ""
            while True:
                sub_res = self._read_channel(loop_delay, strict)
                sub_res_split = sub_res.split(self.response_return)
                if self.pagination_pattern.findall(sub_res_split[-1]):
                    if self.logger:
                        self.logger.info("matched pagination pattern success, get next page of command result")
                    sub_res = self.response_return.join(sub_res_split[0:-1])
                    res += sub_res
                    self._write_channel(self.next_page_cmd)
                else:
                    res += sub_res
                    if self.logger:
                        self.logger.info("no pagination pattern matched, get command result finish")
                    break
        else:
            res = self._read_channel(loop_delay, strict)
        # 检查是否存在命令错误
        if cmd_error:
            self._find_error(res)
        # 返回命令回显
        if self.logger:
            self.logger.info(f"return result of command '`{cmd}`'")
        return res

    def shell_set(self, cmds, ignore=False, loop_delay=0.5, strict=False, cmd_error=True):
        """
        执行一组命令并进行结果检查，若任一命令错误则抛出异常，否则返回一组结果命令回显
        :param [str] cmds:要执行的命令集合
        :param bool ignore: 是否忽略命令错误提示
            True    当前命令出错不会影响下一条命令的执行
            False   当前命令出错，终止剩余命令的执行
        :param float loop_delay: 延迟时间
        :param bool strict: 是否严格执行命令提示符匹配
            True    无法匹配命令提示符时引发超时异常
            False   无法匹配命令提示符时仍会返回结果
        :param bool cmd_error: 是否对命令结果进行异常检查
        """
        if self.logger:
            self.logger.info(f"try to execute a set of commands with param:ignore={ignore}, loop_delay={loop_delay}, strict={strict}, cmd_error={cmd_error}.\ncommands:{cmds}")
        while cmds:
            cmd = cmds.pop(0)
            try:
                res = self.shell(cmd=cmd, loop_delay=loop_delay, strict=strict, cmd_error=cmd_error)
                yield {"state": True, "res": res, "message": ""}
            except Exception as e:
                yield {"state": False, "res": "", "message": repr(e)}
                if not ignore:
                    if self.logger:
                        self.logger.warning(f"error occur while execute command `{cmd}`, terminates execution of the command set")
                    raise e.__class__(repr(e))
        if self.logger:
            self.logger.info("command set execute finish")

    def shell_search_set(self, cmds, ignore=False, loop_delay=0.5, strict=False, cmd_error=True):
        """
        执行一组查询命令并进行结果检查，若任一命令错误则抛出异常，否则返回一组结果命令回显
        :param [str] cmds:要执行的命令集合
        :param bool ignore: 是否忽略命令错误提示
            True    当前命令出错不会影响下一条命令的执行
            False   当前命令出错，终止剩余命令的执行
        :param float loop_delay: 延迟时间
        :param bool strict: 是否严格执行命令提示符匹配
            True    无法匹配命令提示符时引发超时异常
            False   无法匹配命令提示符时仍会返回结果
        :param bool cmd_error: 是否对命令结果进行异常检查
        """
        if self.__class__ == BaseConnection:
            msg = "BaseConnection unsupported function shell_search_set"
            if self.logger:
                self.logger.warning(msg)
            raise UnSupportedException(msg)
        if self.logger:
            self.logger.info(f"try to execute a set of search commands with param:ignore={ignore}, loop_delay={loop_delay}, strict={strict}, cmd_error={cmd_error}.\ncommands:{cmds}")
        self.change_mode("enable")
        while cmds:
            cmd = cmds.pop(0)
            try:
                res = self.shell(cmd=cmd, loop_delay=loop_delay, strict=strict, cmd_error=cmd_error)
                yield {"state": True, "res": res, "message": ""}
            except Exception as e:
                yield {"state": False, "res": "", "message": repr(e)}
                if not ignore:
                    if self.logger:
                        self.logger.error(f"error occur while execute command `{cmd}`, terminates execution of the command set")
                    break
        if self.logger:
            self.logger.info("search command set execute finish")

    def shell_config_set(self, cmds, loop_delay=0.5, strict=False, cmd_error=True):
        """
        执行一组查询命令并进行结果检查，若任一命令错误则抛出异常
        :param [str] cmds:要执行的命令集合
        :param float loop_delay: 延迟时间
        :param bool strict: 是否严格执行命令提示符匹配
            True    无法匹配命令提示符时引发超时异常
            False   无法匹配命令提示符时仍会返回结果
        :param bool cmd_error: 是否对命令结果进行异常检查
        """
        if self.__class__ == BaseConnection:
            msg = "BaseConnection unsupported function shell_config_set"
            if self.logger:
                self.logger.warning(msg)
            raise UnSupportedException(msg)
        if self.logger:
            self.logger.info(f"try to execute a set of config commands with param:loop_delay={loop_delay}, strict={strict}, cmd_error={cmd_error}.\ncommands:{cmds}")
        self.change_mode("config")
        while cmds:
            cmd = cmds.pop(0)
            try:
                res = self.shell(cmd=cmd, loop_delay=loop_delay, strict=strict, cmd_error=cmd_error)
                yield {"state": True, "res": res, "message": ""}
            except Exception as e:
                yield {"state": False, "res": "", "message": repr(e)}
                if self.logger:
                    self.logger.error(f"error occur while execute command `{cmd}`, terminates execution of the command set")
                break
        if self.logger:
            self.logger.info("config command set execute finish")

    def _write_channel(self, cmd):
        """
        写channelia
        :param str cmd: ssh命令
        """
        if not self.transport.active or not self.channel.active or self.channel.closed:
            msg = f"connection to the host closed"
            if self.logger:
                self.logger.critical(msg)
            raise SocketException(msg)
        if self.channel_locked:
            msg = "channel is locked, maybe you should release it by function '_read_channel' or '_clear_buffer'"
            if self.logger:
                self.logger.error(msg)
            raise UnSupportedException(msg)
        self.channel_locked = True   # 锁定channel，防止其他写操作
        if self.logger:
            self.logger.info(f"lock channel")
        self.channel.send(cmd + self.send_return)
        if self.logger:
            self.logger.info(f"send command `{cmd}`")

    def _read_channel(self, loop_delay=0.5, strict=False):
        """
        读channel直至遇到命令提示符或超时
        :param float loop_delay: 延迟时间
        :param bool strict: 是否严格执行命令提示符匹配
            True    超时时引发超时异常
            False   超时时仍会返回结果
        """
        # 若channel未被锁定，说明还未执行写channel,不需要读
        if not self.channel_locked:
            if self.logger:
                self.logger.warning("channel is not locked, can not read channel now")
            return ""
        # 读channel
        res = ""
        if self.logger:
            self.logger.info(f"start to read channel with param:loop_delay={loop_delay}, strict={strict}")
        while self.transport.active and self.channel.active and not self.channel.closed:
            time.sleep(loop_delay)
            try:
                buff = self.channel.recv(self.buffer_size)
            except socket.timeout:
                msg = "no command prompt found until read channel timeout"
                if self.logger:
                    self.logger.warning(msg)
                # 清空buffer，确保已读取channel中所有信息
                final_res = self._clear_buffer()
                # 根据strict处理最后一次读操作
                if strict:
                    raise IOError(msg)
                else:
                    res += final_res
                    break
            else:
                if self.logger:
                    self.logger.debug(f"read {len(buff)}bytes from channel")
                if buff:
                    output = self._decode_res_byte(buff)
                    res += output
                    res_last_line = res.split(self.response_return)[-1]
                    if self._find_prompt(res_last_line):
                        if self.logger:
                            self.logger.debug("matched command prompt, read channel finish")
                        break
                else:
                    if self.logger:
                        self.logger.debug("read 0  bytes from channel, read channel finish")
                    break
        else:
            msg = f"connection to the host closed"
            if self.logger:
                self.logger.error(msg)
            raise SocketException(msg)
        # 返回结果
        self.channel_locked = False
        if self.logger:
            self.logger.info("release channel")
        return res

    def _read_channel_until_timeout(self, loop_delay=0.5):
        """
        读channel直至超时
        :param float loop_delay: 延迟时间
        """
        # 若channel未被锁定，说明还未执行写channel,不需要读
        if not self.channel_locked:
            if self.logger:
                self.logger.debug("channel is not locked, can not read channel now")
            return ""
        # 读channel
        if self.logger:
            self.logger.info(f"start to read channel with param:loop_delay={loop_delay}")
        res = ""
        while self.transport.active and self.channel.active and not self.channel.closed:
            time.sleep(loop_delay)
            try:
                buff = self.channel.recv(self.buffer_size)
                if self.logger:
                    self.logger.debug(f"read {len(buff)}bytes from channel")
            except socket.timeout:
                if self.logger:
                    self.logger.info("read channel timeout, read channel finish")
                break
            else:
                output = self._decode_res_byte(buff)
                res += output
        else:
            msg = f"connection to the host closed"
            if self.logger:
                self.logger.error(msg)
            raise SocketException(msg)
        # 返回结果
        self.channel_locked = False
        if self.logger:
            self.logger.info("release channel")
        return res

    def _decode_res_byte(self, res_byte):
        """
        将二进制数据进行解码
        替换\r*\n为用户指定的换行符
        去掉解码失败的字符
        """
        # 识别编码
        encoding = chardet.detect(res_byte)
        if encoding["confidence"] < 0.9:
            if self.logger:
                self.logger.warning(f"confidence of {encoding['encoding']} is less than 0.9, use default encoding {self.encoding}")
            encoding = self.encoding
        elif encoding["encoding"] not in ENCODINGS:
            if self.logger:
                self.logger.warning(f"not a commonly used encoding {encoding['encoding']}, use default encoding {self.encoding}")
            encoding = self.encoding
        else:
            if self.logger:
                self.logger.info(f"confidence of {encoding['encoding']} is {encoding['confidence']}")
            encoding = encoding["encoding"]
        # 解码
        res_str = res_byte.decode(encoding=encoding, errors="ignore")
        if self.logger:
            self.logger.debug(f"decode {len(res_byte)}bytes")
        # 去掉解码后多出的字符
        res_str = re.sub(r" \[1D|\[K|\x1b\[1D|\x00|\[7?m||\[\?1h=|\[\?1l>", "", res_str, re.S)
        if self.logger:
            self.logger.debug("replaced some invisible chars")
        # 替换换行符
        res_str = re.sub(r"\r*\n\r*", self.response_return, res_str)
        if self.logger:
            self.logger.debug(r"replace the newline")
        return res_str

    def _find_prompt(self, output):
        """
        查询命令提示符
        :param str output: 带查询命令回显缓存
        """
        for pattern in self.prompt_patterns:
            match = pattern.findall(output)
            if match:
                if self.logger:
                    self.logger.debug("the command prompt was matched")
                return True
        if self.logger:
            self.logger.debug("the command prompt was not matched")
        return False

    def _find_error(self, res):
        """
        查询是否存在命令报错
        :param str res: 带查询命令回显
        """
        for pattern in self.error_patterns:
            match = pattern.findall(res)
            if match:
                if self.logger:
                    self.logger.warning(f"find command execution error:\n{res}")
                raise CommandExecuteException(res)
        if self.logger:
            self.logger.debug("command execution result is OK")

    """配置保存"""

    def save(self):
        """保存配置"""
        msg = "BaseConnection unsupported function save"
        if self.logger:
            self.logger.warning(msg)
        raise UnSupportedException(msg)

    """扩展功能"""

    def ping_on_current_host(self, dest_addr=None, unit="ms", size=56):
        """
        在当前主机上执行ping操作并获取ping时延,一般用于登录前网络测试
        :param str dest_addr:ping的目标ip或域名
        :param str unit:ping时延单位，可选：s|ms，默认ms
        :param int size:ping的包大小，默认小包
        :return: 可以ping通时返回以毫秒为单位的时延，无法ping通时返回None
        """
        if not dest_addr:
            msg = "dest_addr must be set"
            if self.logger:
                self.logger.warning(msg)
            raise ValueError(msg)
        try:
            delay = ping(dest_addr=dest_addr, unit=unit, size=size)
            msg = f"ping delay is {delay}{unit} to dest_addr {dest_addr} with package size {size}"
            if self.logger:
                self.logger.debug(msg)
            return delay
        except Exception as e:
            if self.logger:
                self.logger.warning(repr(e))
            raise e

    @property
    def platform(self):
        """
        获取设备平台(操作系统)类型, 获取不到则返回None
        """
        if not hasattr(self, "__platform"):
            self.__platform = None
        if self.logger:
            self.logger.debug(f"return __platform: {self.__platform}")
        return self.__platform



